package top.duorhs.jspider;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpGlobalConfig;
import cn.hutool.http.HttpUtil;
import lombok.Data;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import top.duorhs.jspider.constant.HtmlConstant;
import top.duorhs.jspider.filter.Filter;
import top.duorhs.jspider.output.FileWriter;
import top.duorhs.jspider.output.MarkdownFileWriter;
import top.duorhs.jspider.util.SpiderUtil;

import java.io.File;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author Duorhs
 * @date 2023/1/7 14:25
 */
@Data
public class Spider {
    /**
     * 域名
     */
    private final String host;

    /**
     * 入口网页节点
     */
    private final PageNode root;

    /**
     * 输出处理器
     */
    private final FileWriter fileWriter = new MarkdownFileWriter();

    /**
     * 处理非法文件名
     */
    private final Pattern pattern = Pattern.compile("[\\s\\\\/:*?\"<>|]");

    /**
     * 输出内容
     */
    private LinkedList<String> content = new LinkedList<>();

    /**
     * html内容过滤
     *
     * @see Filter
     */
    private List<Filter> filters;

    /**
     * 遍历深度
     */
    private int depth;

    /**
     * 打印层数
     */
    private int printLevel = 0;

    /**
     * 记录一下已处理的节点数
     */
    private int progress = 0;

    /**
     * 爬虫任务列表，异步线程监听和进行爬取<br/>
     * 由遍历深度和当前列表空判断决定是否全部完成
     */
    private LinkedList<CrawTask> taskQueue = new LinkedList<>();

    /**
     * 用于测试 限制子页面数量，设置为-1则不限制
     */
    private int limit = 10;

    /**
     * 无过滤爬虫，采用默认配置爬取
     *
     * @param host  域名host
     * @param entry 入口地址
     */
    public Spider(String host, String entry) {
        this.host = host;
        this.root = new PageNode(entry);
        this.filters = null;
    }

    /**
     * TODO 带有自定义过滤要求，请参阅 {@link Filter}
     *
     * @param host    域名host
     * @param entry   入口地址
     * @param filters 过滤器
     */
    public Spider(String host, String entry, Filter... filters) {
        this(host, entry);
        this.filters = Arrays.stream(filters).collect(Collectors.toList());
    }

    /**
     * 开始爬取网页<br/>
     *
     * @param depth       最大深度，从入口地址开始，当前页面为第一层，页面中的所有同域url为第二层，以此类推。<br/>
     *                    比如depth=1,则只爬取当前的入口网站<br/>
     *                    注意：过大的深度会严重加大请求时长
     * @param printAllUrl 打印url遍历结果
     * @param savePath    结果保存文件夹，null则不保存，也不会走内容解析的逻辑(因为不需要看结果)<br/>
     *                    空字符串""则保存到当前目录下<br/>
     *                    根目录会以主域名命名，每一层子文件夹则加上自身的路径<br/>
     *                    为了方便展示，保存的格式为MarkDown，图片会保存至 每个目录的images文件夹中并在MD文件中展示
     */
    public void start(int depth, boolean printAllUrl, String savePath) {
        this.depth = depth;
        LinkedList<PageNode> list = new LinkedList<>();
        list.add(root);
        CrawTask task = new CrawTask(1, list);
        taskQueue.addLast(task);
        HttpGlobalConfig.setMaxRedirectCount(2);
        startCraw(savePath);
        if (printAllUrl) {
            printNodes(root, 1);
        }
    }

    //----------------私有方法-----------------------

    /**
     * 爬取，采用任务提交形式，如果当前页面节点存在子节点的话，提交到任务列表
     * TODO 目前只支持GET
     */
    private void startCraw(String savePtah) {
        String outputRoot = null;
        if (savePtah != null) {
            String dirName = SpiderUtil.getHostInUrl(host);
            outputRoot = FileUtil.mkdir(new File(savePtah).getAbsolutePath() + File.separator + dirName).getAbsolutePath();
        }
        while (!taskQueue.isEmpty()) {
            //还有爬虫任务，看看最顶层的任务还有没有需要处理的node
            CrawTask task = taskQueue.peekFirst();
            Integer level = task.getLevel();
            LinkedList<PageNode> taskNodes = task.getNodes();
            if (taskNodes.isEmpty()) {
                //该层已经没有节点了，删除这层的task
                taskQueue.removeFirst();
            } else {
                //该层还有节点，进行遍历，直接把节点从该层的节点列表中取出
                while (!taskNodes.isEmpty()) {
                    PageNode node = taskNodes.pollFirst();
                    String url = node.getCurrentPage();
                    String resp = HttpUtil.get(url);
                    if (SpiderUtil.isHtml(resp)) {
                        Document doc = Jsoup.parse(resp);
                        handleChildNode(node, doc, level);
                        if (outputRoot != null) {
                            handleHtmlContent(doc);
                        }
                    }
                    //改节点处理完毕，写出文件，清除content
                    if (outputRoot != null) {
                        String childUrl = SpiderUtil.cutChildUrl(host, node.getCurrentPage());
                        childUrl = pattern.matcher(childUrl).replaceAll("");
                        fileWriter.write(outputRoot + File.separator + childUrl, content);
                        content = new LinkedList<>();
                    }
                }
            }
        }
    }


    /**
     * 处理子节点逻辑
     *
     * @param node
     * @param doc
     * @param level
     */
    private void handleChildNode(PageNode node, Document doc, int level) {
        //判断当前层数是否为最大深度，如果是，就只为当前节点加上子节点信息，但是不提交字节点任务
        LinkedList<PageNode> hrefs = getHrefs(doc);
        if (hrefs != null) {
            node.addNodes(hrefs);
        }
        if (level != this.depth) {
            //提交子节点任务
            if (taskQueue.size() < depth) {
                CrawTask nextLevelTask = new CrawTask(level + 1, new LinkedList<>(node.getChildPages()));
                taskQueue.addLast(nextLevelTask);
            } else {
                taskQueue.get(level).getNodes().addAll(node.getChildPages());
            }
        }
    }

    /**
     * 解析HTML内容，保存到文件中
     *
     * @param doc doc对象
     */
    private void handleHtmlContent(Document doc) {
        Elements htmlBodies = doc.getElementsByTag("body");
        for (Element body : htmlBodies) {
            parseHtml(body);
        }
    }

    /**
     * 解析HTML的DOC，爬取所有超链接
     *
     * @param document HTML页面DOC
     * @return 解析列表
     */
    private LinkedList<PageNode> getHrefs(Document document) {
        LinkedList<PageNode> list = new LinkedList<>();
        Elements elements = document.getElementsByAttribute(HtmlConstant.ATTR_HREF);
        for (Element element : elements) {
            String href = element.attr(HtmlConstant.ATTR_HREF);
            String realUrl = SpiderUtil.toCurrentHost(host, href);
            if (realUrl != null) {
                list.add(new PageNode(realUrl));
            }
        }
        return list.isEmpty() ? null : list;
    }

    /**
     * 递归调用函数<br/>
     * 解析HTML的DOC
     *
     * @param node html-body
     */
    private void parseHtml(Node node) {
        if (node.childNodeSize() != 0) {
            for (Node childNode : node.childNodes()) {
                parseHtml(childNode);
            }
        } else {
            if (node instanceof Element) {
                Element element = (Element) node;
                String text = element.text();
                if (!StrUtil.isBlank(text)) {
                    content.add(text);
                }
                if (HtmlConstant.TAG_IMG.equals(element.tagName())) {
                    content.add(node.attr("src"));
                }
            } else if (node instanceof TextNode) {
                TextNode textNode = (TextNode) node;
                if (!textNode.isBlank()) {
                    content.add(textNode.text());
                }
            }
        }
    }


    /**
     * 在当前的深度要求下打印所有爬取到的url
     * TODO 还没做完
     */
    private void printNodes(PageNode node, int level) {
        if (printLevel != level) {
            printLevel++;
            System.err.println(StrUtil.format("Pages in level {}", printLevel));
        }
        System.out.println(node.getCurrentPage());
        if (node.hasChild()) {
            level++;
            for (PageNode childPage : node.getChildPages()) {
                printNodes(childPage, level);
            }
        }
    }
}
